Djangoのクラスベースのビューの探訪 Part 1
著者:Leonardo Giordani - 28/10/2013 Updated on Mar 16, 2020
はじめに
Django 3が2019年末にリリースされたので、Djangoのクラスベースのビューに関する私の成功した一連の投稿を再訪する時期が来たと思います。これらの投稿は2013年にさかのぼり、Django 1.5を念頭に置いて、そのコードベースの例を使って書かれています。現在、Djangoはすでに2つのバージョンになっていますが、クラスベースのビューは依然としてフレームワークの大部分を占めているので、これらの投稿の内容を更新することは意味があると考えています。さらに、私はまだDjango 3を勉強する機会がありませんでしたので、このブログの伝統に従って、私の個人的な調査を誰にでも公開します。
もしあなたが Python プログラマーの初心者で、ウェブ開発のキャリアを始めるために Django に近づいたばかりであれば、多くのことに戸惑ったことでしょう。CBV は、単純なケースでは非常に使いやすいのですが、プロジェクトの開発が進むにつれて、より複雑なユースケースに合わせて拡張する方法がわからなくなることがあります。公式ドキュメントは非常に優れていますが、CBVを使いこなすには、クラス(当然ですが)、デリゲーション、メソッドのオーバーライドといったオブジェクト指向の概念を理解する必要があります。
もしこれらの概念をブラッシュアップする必要があれば、このブログの以下の記事を読むと役に立つかもしれません。
CBV って何ですか?
クラスベースビュー(Class Base View)とは、Python のクラスをベースにした Django のビューです。つまり、CBV を使いこなすには、Django のビューと Python のクラスの両方を理解する必要がありますので、簡単に定義しておきましょう。
Django のビューとは、入力された HTTP リクエストを処理して HTTP レスポンスを返すコードの断片であり、それ以上でもそれ以下でもありません。Python クラスとは、オブジェクト指向の概念であるクラスを Python 言語で実装したものです。
つまり、ビューは呼び出し可能である必要があり、これには関数やクラスも含まれます。したがって、関数ベースのビューに対するクラスベースのビューのメリットを理解するために、関数に対するクラスのメリットを説明します。後半の文章は、プログラミングに関する10巻の本のタイトルになりそうなので(続いて「クラスに対する関数のメリット」というタイトルの別の10巻の本が出ます)、ここでは表面をなぞるだけにしておきます。もっと詳しく知りたい方は、上記のリンク先にあるPython 3 OOPのシリーズをお読みください、そこにはあなたが切望する全ての血なまぐさい詳細が書かれています。
Pythonのクラスを始めよう
クラスの主な目的はカプセル化の実装で、データと関数を結合する方法を表しています。これを行うことで、クラスは、実行中にのみ存在するプロシージャの動的な本質を失い、生きた実体になります。つまり、そこに座ってデータを管理し、関数(メソッド)を呼び出すと反応するものになります。
クラスの良い例えは、有限状態の機械です。クラスが初期化されると、機械が状態間を移動するためにメソッドが使われます。もし、メソッドを呼ばなければ、クラスは文句を言わずにただ待っているだけです。
例として、リストのような反復可能な要素から偶数を抽出する非常に単純な手続きを見てみましょう。
code: python
def extract_even_numbers(alist):
この例は非常に小さなものですが、コードが複雑になるのは当然のことなので、まずは簡単な例から始めるのが良いでしょう。この関数のクラスバージョンは次のように書くことができます。
code: python
class EvenExtractor:
def __init__(self, alist):
self.l = alist
def extract(self):
この2つはよく似ていて、何も変わっていないように見えるかもしれません。実際、その違いは微妙ですが注目に値します。
次の3つの状態があります。
初期化前: EvenExtractor
初期化後: e = EvenExtractor([1,4,5,7,12])
抽出後: l = e.extract()
手続きをクラスに変換することで、ステップごとに仕事を実行できるリッチなツールが得られ、一般的には、さらにメソッドを追加したり、より多くの状態を追加したりすることができるため、非線形な方法で作業することができます。
委譲が鍵となる
有限状態マシン(Finite state machine) (有限オートマトン(finite automaton)とも呼ばれる)として使用されるクラスの真の力は、委譲(Delegation)の概念にあります。これは、クラスがある作業を他のクラスに委ねることができるメカニズムで、コードの重複を避け、コードの再利用と一般化を促進します。
(私が継承(inheritance)に言及していないことにお気づきかもしれませんが、委譲は合成(Composition)と継承の両方で実装されています。私は「継承よりも合成を優先する(Favour composition over inheritance)」というOO設計原則を強く支持しています。継承のメカニズムを強調しすぎて、合成を無視したオブジェクト指向の入門書を読み続けていると、多くの小さな共同作業を行うオブジェクトで合成されたシステムを構築する代わりに、時にはシステムコンポーネントというよりもオペレーティングシステムに似た巨大な万能物がはびこる悪夢を生み出すOOPプログラマーの世代を育てることになってしまうのです(笑)。
上の例の続きで、EvenExtractorクラスの__init__メソッドを改良してみましょう。
code: python
class EvenExtractor:
def __init__(self, alist):
def extract(self):
このクラスは、初期化段階で入力のすべての要素を整数に変換するという重要な動作を行うようになりました。しかし、この変更の数日後には、リストから奇数の要素を抽出するクラスも有益に利用できることに気づくかもしれません。責任あるオブジェクト指向プログラマーとして、私たちはそれを書いておくべきです。
code: python
class OddExtractor(EvenExtractor):
def extract(self):
新しいクラスのその(EvenExtractor)シグネチャによって表現された継承メカニズムを通じて、私たちはまず、EvenExtractorとまったく同じものを、同じメソッドと属性を持ちながら、異なる名前で定義しました。そして、そのメソッドをオーバーライドすることで、抽出部分のみ、新しいクラスの動作を変更しました。
ここまでを要約すると、クラスとデリゲーションを使って、ニーズに合わせて簡単にカスタマイズできる有限状態マシンを構築することができます。これは明らかに、クラスを考える上での多くの視点の一つに過ぎませんが、 Django の CBV を理解するためには必要な視点なのです。
Djangoに戻しましょう
これまでに学んだことの実用的な使い方について、Django が Python のクラスと委譲を使ってビューを提供する方法を復習しながら、議論を始めましょう。
Django のビューは有限状態の機械の完璧な例です。入力されたリクエストを受け取り、最終的なレスポンスが生成されるまで、様々な処理ステップを経て、ユーザーに送り返します。CBVは、プログラマーがオブジェクト指向のパラダイムを利用してビューを書くための方法です。ここで言うクラスベースジェネリックビューとは、Django のビューの「付属品」であり、フレームワークが最初から用意しているビルディングブロックのことです。
Django の公式ドキュメントに掲載されている例 を見てみましょう。ここには、データベースから抽出したリストを扱う汎用ビューである、愛すべき ListView の API が掲載されています。ここでは、ドキュメントに掲載されている例を少し簡略化して、多くのことを考えないようにしました。 code: python
from django.views.generic.list import ListView
from articles.models import Article
class ArticleListView(ListView):
model = Article
この例では、Articleがあなたのアプリケーションであり、Articleがそのモデルの1つであると仮定しています。
ここでは、継承の完全な力を見ることができます。ListViewからArticleListViewを派生させ、modelクラス属性を変更しただけです。これはどのように動作しますか?このクラスはどのように受信要求を処理し、何を出力するのでしょうか?公式ドキュメントは「このビューが実行している間、object_listはビューが操作しているオブジェクトのリスト(通常、必ずしもクエリセットではない)を含む」と述べているが、これは多くの暗いコーナーを残しており、あなたが初心者であれば、すでに失われている可能性がある。
ArticleListViewはListViewから派生しているので、後者は、入力データがどのように処理されるかを理解するために分析しなければならないクラスです。これを行うには、ドキュメント を見る必要があり、何かがまだ不明瞭である場合は、自由にソースコード を見ることができます。以下の段落では、上に示した ArticleListView クラスのサンプルを Django が呼び出したときに何が起こるかをまとめます。また、ご自分で読みたい場合は、リンク「DOCS」から公式ドキュメントを、リンク「CODE」から関連するソースコードを読むことができます。 URLディスパッチャとビュー
その代わり、as_viewメソッド(CODE)の結果を渡さなければなりません。as_viewメソッドは、クラス(CODE)をインスタンス化し、dispatchメソッド(CODE)を呼び出す関数を定義しており、その関数はURLディスパッチャで使用するために(CODE)返されます。ユーザーとしては、クラスのエントリーポイント(リンクされたURLにリクエストがヒットしたときに呼び出されるメソッド)がディスパッチであるということだけに興味があります。 この知識を使って、CBV でリクエストが処理されるたびに、コンソールに文字列を出力してみましょう。実際の問題を解決するときに、CBV をどのように扱わなければならないかを正確に示しているので、この単純なタスクを段階的に実行していきます。
ArticleListViewクラスをこのように定義すると
code: python
from django.views.generic.list import ListView
from articles.models import Article
class ArticleListView(ListView):
model = Article
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
クラスはその動作を変更しません。私たちが行ったのは、親のメソッドの呼び出しでディスパッチメソッドをオーバーライドすること、つまりPythonがデフォルトで行うことを明示的に書いたのです。super()についての詳しい情報は、公式ドキュメント や次のブログの記事にあります。 可変引数を定義するための*と**の記法を理解しておいてください。公式ドキュメントを参照してください。 ビューはフレームワークから自動的に呼び出されるため、フレームワークはビューが非常に特殊なAPIに準拠することを期待しています。したがって、メソッドをオーバーライドする際には、元のメソッドと同じシグネチャを提供する必要があります。ディスパッチのシグネチャはドキュメント にあります。 dispatchメソッドはリクエストの引数を受け取りますが、そのタイプはHttpRequest(ドキュメント)なので、標準のprint関数を使ってコンソールに表示することができます。 code: python
from django.views.generic.list import ListView
from articles.models import Article
class ArticleListView(ListView):
model = Article
def dispatch(self, request, *args, **kwargs):
print(request)
return super().dispatch(request, *args, **kwargs)
これにより、リクエストオブジェクトの内容が、Django プロジェクトを実行しているサーバの標準出力に表示されます。Django の開発サーバを動かしている場合は、python manage.py runserver というコマンドを発行したテキストコンソールに出力されます。
一言で言えば、これが Django の CBGV を扱う標準的な方法です。定義済みのクラスを継承し、変更する必要のあるメソッドを特定し、シグネチャに準拠してオーバーライドし、新しいコードのどこかで親のコードを呼び出すのです。
ListViewが受信リクエストを処理する際に使用するメソッドの完全なリストは、その公式ドキュメントの "Method Flowchart "セクションに記載されています。"Ancestors (MRO) "セクションでは、ListViewが他の多くのクラスを継承していることがわかります。MROはMethod Resolution Orderの略で、多重継承を扱う必要があります。Pythonの最も複雑なトピックの1つを扱いたい方は、チュートリアルの多重継承 のセクションを気軽にお読みください。 GETリクエストの着信
ArticleListViewに戻りましょう。親のディスパッチメソッドはリクエストオブジェクトのmethod属性を読み、リクエスト自体を処理するハンドラを選択します(CODE)。つまり、request.methodが'GET'の場合、これはリソースを読んでいることを示すHTTPの方法ですが、ディスパッチはクラスのgetメソッドを呼び出します。 ListViewのgetメソッドは、その祖先であるBaseListViewから来ています(DOCS, CODE)。ご覧のように、この関数は基本的に、get_queryset()を呼び出した結果で属性object_listを初期化し、メソッドget_context_dataを呼び出してコンテキストを作成し、render_to_responseを呼び出しています。 まだ私と一緒にいてくれますか?あきらめないでください。少なくともListViewについては、ほとんど終わっています。get_querysetメソッドはListViewのMultipleObjectMixinの祖先(DOCS, CODE)から来ており、単純に queryset = self.model._default_manager.all() を実行して、与えられたモデル(CODE)の全てのオブジェクトを取得します。モデルの値は、model = Article と書いたときにクラスで設定したものです。この時点で、あなたの頭の中で何かが理解され始めていることを願っています。 実際、それだけです。ArticleListViewクラスは、データベースからすべてのArticleオブジェクトを抽出し、抽出されたオブジェクトのリストでインスタンス化された1つの変数、object_listを含むコンテキストを渡すテンプレートを呼び出します。
テンプレートとコンテクスト
満足されましたか?実は、テンプレートとコンテキストについてはまだ興味があります。では、これらのトピックについて何がわかるか見てみましょう。まず、クラスがrender_to_responseを呼び出す際には、TemplateResponseMixinの祖先(DOCS、CODE)に由来するコードを使用します。このメソッドは、テンプレートとコンテキストを渡してクラスTemplateResponseを初期化します。TemplateResponseMixinがそれをNoneとして初期化している間(CODE)、ListViewは先祖(CODE)を通じていくつかのマジックを行い、与えられたモデルから派生したテンプレートを返します。要するに、Articleモデルを定義したArticleListViewは、自動的にarticle_list.htmlと呼ばれるテンプレートを使用します。 この動作を変更してもいいですか?もちろんです。これは、結局、関数の代わりにクラスを使用するポイントです:簡単にカスタマイズ可能な動作です。クラスの定義を次のように変更することができます。
code: python
from django.views.generic.list import ListView
from articles.models import Article
class ArticleListView(ListView):
model = Article
template_name = 'sometemplate.html'
これが何をしているのか、順を追って確認してみましょう。レスポンスが作成されると、 Django は render_to_response (CODE) のコードを実行し、その中で get_template_names が呼び出されます。このメソッドは名前のリストを返しますが、 Django はその中で最初に利用可能なものを使い、順番にスキャンしていくので注意してください。このメソッドは ListView のスーパークラスである MultipleObjectTemplateResponseMixin (CODE) によってオーバーライドされています。これは、自身のスーパークラスTemplateResponseMixin(CODE)の同じメソッドを呼び出し、ArticleListViewクラス(CODE)で設定した属性を返します。ミキシングは続けて、モデルから派生したテンプレートファイル名をリストに追加し(CODE)、最後にリストを返します。この時点でのリストは ['sometplate.html', 'article_list.html'] です。 コンテキストについては、テンプレートをコンパイルする際にアクセスできるようにしたい値の辞書に過ぎないことを覚えておいてください。コンテキスト内の変数名、データの形式、データの内容は完全にあなた次第です。しかし、CBGVを使用している場合は、object_listのように、ビューの祖先によって作成された変数がコンテキストの中に含まれていることがあります。すべての記事のリストを表示するページを表示したいが、コンテキストに値を追加したい場合はどうすればよいでしょうか。
簡単なことです。コンテキストを生成する関数をオーバーライドして、その動作を変更すればよいのです。例えば、サイトの全読者数を記事の一覧と一緒に表示したいとします。読者モデルが利用可能であると仮定すると、次のように書くことができます。
code: python
from django.views.generic.list import ListView
from articles.models import Article, Reader
class ArticleListView(ListView):
model = Article
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
return context
いつものように、メソッドをオーバーライドする際には、元のメソッドを呼び出す必要があるかどうかを自問する必要があります。このケースでは、コンテキストの内容を置き換えるのではなく、単に追加したいので、まず super().get_context_data(**kwargs) を呼び出し、そこに必要な値を追加しています。
最後に
この最初の投稿では、Django の CBV と CBGV にまつわる謎のいくつかを解き明かそうとし、クラスベースのビューにヒットした GET リクエストに何が起こるかを正確に示しました。これで、この問題が少しは解明されたでしょうか?
次回は、オブジェクトの詳細を表示する汎用ビューである DetailView、カスタム CBV の作成方法、そして CBV を使ってフォームを処理する方法、つまり POST リクエストを受け付ける方法について説明します。
Digging up Django class-based views series
日本語訳::Djangoのクラスベースのビューの探訪 Part 1